Skip to content

[TV] Search: Podcasts/Episodes scope filter - #5729

Merged
sztomek merged 4 commits into
mainfrom
feat/tv-search-filter
Aug 14, 2026
Merged

[TV] Search: Podcasts/Episodes scope filter#5729
sztomek merged 4 commits into
mainfrom
feat/tv-search-filter

Conversation

@sztomek

@sztomek sztomek commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Description

Adds the Podcasts/Episodes scope filter to the TV Search screen, on top of the basic search PR.

Stacked on #5728 (feat/tv-search-results). Review/merge that one first.

  • Pill filterTop Results / Podcasts / Episodes, styled exactly as the top‑bar tab pills (androidx.tv.material3 TabRow + pill indicator in a backgroundSunken container), centered between two divider lines per the Figma. Selecting follows focus, like the top bar.
  • Scopes:
    • Top Results — the combined view (podcasts carousel + a capped episodes preview).
    • Podcasts — a 6‑column cover grid (TvPodcastGridScaffold).
    • Episodes — the full episode list.
  • Adds filter state + onFilterSelected to TvSearchViewModel and the scope switching in TvSearchScreen. No new strings (reuses search_filters_*), no analytics.

Fixes POC-800 https://linear.app/a8c/issue/POC-800/wire-up-search-apis
Figma: Ftk3KwnfqaK4g57yCN63p0-fi-2595_2243

Testing Instructions

  1. Build the branch (stacked on [TV] Search: combined search results (podcasts + episodes) #5728) and open the Search tab; type a query.
  2. A centered Top Results / Podcasts / Episodes pill bar appears between two divider lines.
  3. D‑pad down from the field lands on the filter; arrow left/right to switch scope (selects on focus).
  4. Top Results shows podcasts + an episodes preview; Podcasts shows the cover grid; Episodes shows the full list.
  5. Clearing the query hides the filter and returns to the Discover browse.

Screenshots or Screencast

Screen_recording_20260812_142126.mp4

Checklist

  • If this is a user-facing change, I have added an entry in CHANGELOG.md
  • Ensure the linter passes (./gradlew spotlessApply to automatically apply formatting/linting)
  • I have considered whether it makes sense to add tests for my changes
  • All strings that need to be localized are in modules/services/localization/src/main/res/values/strings.xml
  • Any jetpack compose components I added or changed are covered by compose previews
  • I have updated (or requested that someone edit) the Event Horizon schema to reflect any new or changed analytics.

I have tested any UI changes...

  • with different themes
  • with a landscape orientation
  • with the device set to have a large display and font size
  • for accessibility with TalkBack

@sztomek
sztomek force-pushed the feat/tv-search-filter branch from af82e68 to 3414c70 Compare August 12, 2026 11:49
@sztomek sztomek added this to the 8.19 milestone Aug 12, 2026
@sztomek
sztomek marked this pull request as ready for review August 12, 2026 12:05
@sztomek
sztomek requested a review from a team as a code owner August 12, 2026 12:05
@sztomek
sztomek requested review from geekygecko and removed request for a team August 12, 2026 12:05
@sztomek

sztomek commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

@claude please review this

@claude

claude Bot commented Aug 12, 2026

Copy link
Copy Markdown

Claude finished @sztomek's task in 2m 33s —— View job


Code review: TV Search Podcasts/Episodes scope filter

  • Gather context (diff vs feat/tv-search-results)
  • Review TvSearchFilters.kt, TvSearchScreen.kt, TvSearchViewModel.kt, TvPodcastGridScaffold.kt
  • Post review

Nice, focused change — the scope switching reads cleanly and splitting TvSearchResults into TvSearchTopResults / TvSearchEpisodeList was the right call. A few things worth looking at before merge, in rough priority order.


1. Focus is lost in the Episodes scope after the podcast overlay closes

restoreFocusTrigger exists precisely because tvFocusInactiveWhen(podcastUuid != null) (TvSearchScreen.kt:108) strips focus from the content while the detail overlay is up, and onHide (:113) bumps the trigger so something below re-claims it.

TvSearchTopResults consumes it (:329) and TvPodcastGridScaffold consumes it (:290), but TvSearchEpisodeList (:368-385) doesn't take it at all. Repro on the Episodes tab: focus an episode → open actions → Go to podcast → back out of the podcast detail. The overlay closes, nothing requests focus, and the D-pad goes dead until the user finds another way in.

Related, milder: in TvSearchTopResults the restore always targets podcastsRowFocusRequester, which is only attached when podcasts.isNotEmpty() (:338). Episode-only results hit the same dead-end — runCatching (:333) swallows it silently. Worth a fallback to the first episode row.

Fix this →

2. No explicit focus requester on the selected pill

TvTabBar pins focus to the selected tab (TvTabBar.kt:107, .then(if (index == selectedTabIndex) Modifier.focusRequester(focusRequester) else Modifier)). TvSearchFilterPills relies on focusRestorer() alone.

Since selection follows focus here, any case where the restorer's saved child is dropped means focus lands on Top Results and the whole content region silently swaps out from under the user. Worth exercising: select Episodes → D-pad down into the list → scroll a few items → D-pad up back to the pills. If focus doesn't return to Episodes, the scope resets.

3. title = "" as a "no title" sentinel

TvSearchScreen.kt:287 passes an empty string, and TvPodcastGridScaffold.kt:41 now branches on title.isNotEmpty(). An empty-string sentinel is easy to trip over — title: String? = null expresses it directly and lets the search call site just omit the argument. (The other two call sites, TvFolderDetailScreen.kt:98 and TvYourPodcastsScreen.kt:162, pass real titles and are unaffected.)

Fix this →

4. Podcasts grid is misaligned with the rest of the search screen

The search field, filter row and Top Results carousel all sit at 48dp (ContentPadding, TvSearchScreen.kt:63), but TvPodcastGridScaffold hardcodes contentPadding = PaddingValues(start = 32.dp, …, end = 32.dp) (TvPodcastGridScaffold.kt:77). Switching Top Results → Podcasts shifts the covers 16dp left relative to the pills above them. Either plumb a contentPadding parameter through the scaffold or wrap the grid — worth a quick check against the Figma.

5. Duplicated pill-bar implementation

TvSearchFilterPills (TvSearchFilters.kt:52-108) is a near-verbatim copy of TvTabBar.kt:79-157 — same Box + backgroundSunken + RoundedCornerShape(percent = 50) + 3.dp padding, same TabRow/PillIndicator config, same TabDefaults.pillIndicatorTabColors five-colour block, same 44.dp/21.dp metrics, same inner Box + caption1 Text. That's the styling contract for TV pills duplicated in two files, so any design tweak now has to land in both.

Extracting a shared TvPillTabRow(labels, selectedIndex, onSelect, …) into component/ would let both call sites keep only their own focus behaviour. Not blocking, but the divergence risk is real given how mechanically identical these are.

6. Filter shown for Searching / Error / NoResults

searchState !is TvSearchState.Idle (TvSearchScreen.kt:170) also covers the loading spinner, the error message and the no-results message. In all three the pills are interactive but change nothing, and on Error/NoResults they're a focusable row leading to identical content. Intentional (avoids the row flickering on every keystroke while Searching), or should it be is TvSearchState.Results?

7. Smaller notes

  • Modifier.padding(horizontal = 48.dp) at TvSearchScreen.kt:174 duplicates the ContentPadding constant declared 111 lines above — use Modifier.padding(ContentPadding) so they can't drift.
  • The no-results message is written three times (:281-284, :304-307) with identical arguments. A local @Composable fun NoResults() or hoisting it above the when would cut the repetition.
  • Episode item rendering is duplicated between TvSearchTopResults (:350-361) and TvSearchEpisodeList (:374-382) — a LazyListScope.tvSearchEpisodeItems(episodes, …) extension, mirroring the existing tvSearchPodcastsRow, would cover both.
  • color = LocalContentColor.current on the Text (TvSearchFilters.kt:97) is the default already — harmless, and it matches TvTabBar, so only worth dropping if you extract the shared component.
  • Preview coverage: only TvSearchFilter.Podcasts is previewed. A TopResults variant would catch first-pill indicator/rounding issues.

8. Tests

TvSearchViewModelTest is already well built out, and the two new VM behaviours are trivially testable with the existing harness — no new mocks needed:

  • onFilterSelected updates filter.
  • Clearing the query resets filter to TopResults (TvSearchViewModel.kt:104), which is the non-obvious one and easy to regress.

Fix this →


Verdict: the only thing I'd call blocking is #1 — a dead D-pad on TV is a hard stop for the user. #2 is worth a manual pass on device since it's cheap to verify. Everything else is polish and can land as follow-ups.

Note: I reviewed the diff statically and did not build or run the module.
• branch feat/tv-search-filter

@geekygecko geekygecko left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Works well. The only minor change is that it needs to use the grey state similar to Apple TV when you are on the control.

Not hovering:

Image

Hovering:

Image

@sztomek
sztomek force-pushed the feat/tv-search-results branch from 7bc70d4 to 17d971b Compare August 14, 2026 08:01
Base automatically changed from feat/tv-search-results to main August 14, 2026 08:13
@sztomek
sztomek force-pushed the feat/tv-search-filter branch from d134ceb to b88363a Compare August 14, 2026 08:28
@claude

claude Bot commented Aug 14, 2026

Copy link
Copy Markdown

Claude finished @sztomek's task in 3m 51s —— View job


Code review: TV Search Podcasts/Episodes scope filter

  • Gather context (diff vs origin/main, 4 commits)
  • Review TvSearchFilters.kt, TvSearchScreen.kt, TvSearchViewModel.kt, TvPodcastGridScaffold.kt, TvTabBar.kt, TvSearchEpisodeRow.kt
  • Post inline comments + summary

Re-reviewed after the three follow-up commits. The two things I called out last time as the real risks are both fixed: TvSearchEpisodeGrid now takes restoreFocusTrigger and restores through the grid's onEnter (TvSearchScreen.kt:403-410), and TvSearchTopResults falls back to the first episode row when there are no podcasts (:382) — so the dead-D-pad-after-overlay case is closed on every scope. The isInitialComposition guard is duplicated three times now but each copy is correct, and the comment explaining why it exists (TvPodcastGridScaffold.kt:63-64) is the kind of thing that saves the next person an hour.

I've left five inline comments. Summary below, most-significant first.


1. Episodes-scope grid drops the episode-actions affordance (inline)

The new two-column grid reaches onOpenEpisodeActions only through onLongClick (TvSearchScreen.kt:436). Every other episode list in the TV app routes through TvEpisodeListItemContainer, which animates a visible TvMoreButton in on focus — including TvSearchEpisodeRow, so the Top Results scope shows that button for the very same episodes.

The result is that narrowing to the scope dedicated to episodes is what makes the episode actions invisible. A held D-pad centre isn't something a TV user discovers, and there's no on-screen hint. This is the one item I'd want resolved before merge; everything else below is polish.

Secondary effect worth checking on device: once onLongClick is set, a held centre press stops firing onClick, so a press-and-hold opens the actions modal instead of playing.

2. The three scopes don't share a left edge (inline)

  • search field / pills / Top Results carousel — ContentPadding = 48dp (TvSearchScreen.kt:68)
  • Episodes grid — 48dp (:417)
  • Podcasts grid — 32dp, hardcoded in TvPodcastGridScaffold.kt:77

So Top Results → Podcasts shifts the covers 16dp left of the pills directly above them, and Podcasts → Episodes shifts them back. A contentPadding parameter on the scaffold (defaulting to today's 32dp) keeps the other two call sites untouched.

Same comment covers title = "" (:303) as a "no title" sentinel — String? = null says it directly.

3. The pill bar is now duplicated verbatim, and it already cost something (inline)

TvSearchFilterPills (TvSearchFilters.kt:57-106) is identical to TvTabBar.kt:79-146 apart from one .then(…focusRequester…) line. The evidence that this matters is in this PR: @geekygecko's grey-state feedback required the same two-line colour change in both files in commit b88363a. Extracting a shared TvPillTabRow into component/ is cheap now.

4. The tab-bar colour change is global, not search-local (inline)

TvTabBar is the app-wide top nav, so the selected Home/Podcasts/Search pill now renders grey (backgroundBase #292B2E) whenever the bar doesn't hold focus — on every screen. Contrast against the #161718 container is fine and this is clearly the intended design, but it's a global nav change riding in a search PR: worth a note in the description and a QA pass that the current tab still reads at a glance from couch distance while focus sits down in the content.

5. No tests for the new state (inline covers the adjacent nit)

TvSearchViewModelTest has a working harness and neither new behaviour needs a new mock:

  • onFilterSelected updates filter.
  • Clearing the query resets filter to TopResults (TvSearchViewModel.kt:104) — the non-obvious one, invisible from the call site, and exactly the kind of thing a later refactor drops silently.

Add these →

6. Smaller notes

  • Modifier.padding(horizontal = 48.dp) (:188) re-states ContentPadding from :68.
  • Filter row shows for Searching / Error / NoResults (:184) — interactive but inert in all three. Probably deliberate to avoid flicker while Searching; a one-line comment would settle it.
  • The no-results message is written three times (:216-219, :297-300, :320-323) with identical arguments — a local @Composable fun NoResults() collapses it.
  • Preview coverageTvSearchFilters only previews Podcasts (the middle pill). A TopResults variant would catch first-pill indicator/rounding issues, and neither new scope (podcast grid, episode grid) has a preview, so the PR checklist's "any jetpack compose components I added are covered by compose previews" isn't quite met.
  • color = LocalContentColor.current (TvSearchFilters.kt:99) is already the default — only worth dropping as part of the extraction in Updated the release version number #3.
  • No explicit focus requester on the selected pill. TvTabBar pins focus to the selected tab (:107); TvSearchFilterPills relies on focusRestorer() alone. Since selection follows focus, any case where the restorer's saved child is dropped silently resets the scope to Top Results. Cheap to verify: Episodes → down into the grid → scroll a few rows → back up to the pills.

Verdict: #1 is the one I'd fix before merge — it's a real affordance regression inside the feature this PR adds, and it contradicts the same screen one pill over. #2 is a visible 16dp jump that a designer will file anyway. The rest can land as follow-ups.

Reviewed the diff statically — I did not build the module or run it on a device, so the focus-behaviour notes in #6 are reasoning from the code, not observed.
• branch feat/tv-search-filter

Comment thread tv/src/main/java/au/com/shiftyjelly/pocketcasts/home/TvTabBar.kt
@sztomek
sztomek force-pushed the feat/tv-search-filter branch from b88363a to 4cdc38e Compare August 14, 2026 08:42
@claude

claude Bot commented Aug 14, 2026

Copy link
Copy Markdown

Claude finished @sztomek's task in 3m 59s —— View job


Code review: TV Search Podcasts/Episodes scope filter

  • Gather context (diff vs origin/main)
  • Review TvSearchScreen.kt, TvSearchFilters.kt, TvSearchViewModel.kt, TvSearchEpisodeRow.kt, TvPodcastGridScaffold.kt, TvTabBar.kt
  • Post inline comment + summary

Re-reviewed after the amend to 4cdc38e. The only substantive change since my last pass is in TvSearchContent — the root focusGroup() + focusProperties { onEnter = … } was replaced by a LaunchedEffect(Unit) { requestFocus() }.


1. The new focus-entry mechanism (inline)

The intent is right — the old onEnter fired on every re-entry and was fighting restoreFocusTrigger when the podcast overlay closed. Two follow-ons:

  • LaunchedEffect(Unit) + requestFocus() is the racy variant. TvEmptyState.kt:41-44 guards the identical call with withFrameNanos {}, and TvPodcastGridScaffold.kt:55-58 waits on snapshotFlow { …visibleItemsInfo.isNotEmpty() } — this repo already ran into it. Without a guard, runCatching silently swallows the throw and the Search tab opens with nothing focused, i.e. a dead D-pad. Intermittent, so it won't show up reliably in manual QA. One line to fix.
  • tvFocusInactiveWhen lost its focus target. It's focusProperties { canFocus = false } applied to the modifier this Column receives (:114). It used to sit on the .focusGroup() target, which deactivated the whole subtree as the KDoc promises; with the group gone it propagates only as far as the nearest focus targets below. This was the only tvFocusInactiveWhen call site with an explicit focusGroup(). Reasoning from the modifier chain, not from a device — worth confirming that with the detail overlay open the D-pad can't reach the results underneath in all three scopes.

2. Still open from the previous pass

Unchanged in this push, so the earlier inline threads still apply:

  • Episode actions are long-press-only in the Episodes scope (thread) — TvSearchEpisodeCard in the grid reaches onOpenEpisodeActions only via onLongClick (:432), with no visible affordance. The same episodes in Top Results go through TvEpisodeListItemContainer and show a TvMoreButton on focus. So narrowing to the episode-dedicated scope is what removes the discoverable action. Still the one item I'd resolve before merge; at minimum pass an onLongClickLabel for TalkBack.
  • Scopes don't share a left edge (thread) — field/pills/carousel and the episodes grid at 48dp, TvPodcastGridScaffold hardcoded to 32dp (TvPodcastGridScaffold.kt:77). A visible 16dp jump switching Top Results → Podcasts. Same thread covers title = "" (:299) as a "no title" sentinel.
  • TvSearchFilterPills duplicates TvTabBar (thread) — the grey-state feedback already had to be applied twice.
  • Global tab-bar colour change (thread) — TvTabBar.kt:94/110 affects every screen, worth a line in the description.
  • Modifier.padding(horizontal = 48.dp) (:184) still re-states ContentPadding (:68); filter row still renders for Searching/Error/NoResults (thread).

3. Tests

TvSearchViewModelTest has 16 tests and no new mocks are needed for either new behaviour — neither is covered:

  • onFilterSelected updates filter.
  • Clearing the query resets filter to TopResults (TvSearchViewModel.kt:104) — invisible from the call site and exactly what a later refactor drops silently.

Add these →

4. Smaller notes

  • Preview coverage — TvSearchFilters only previews the middle pill (Podcasts); neither new scope (podcast grid, episode grid) has a preview.
  • The no-results message is written three times with identical arguments (:212-215, :293-296, :316-319).
  • No explicit focus requester on the selected pill, unlike TvTabBar.kt:107. Since selection follows focus, a dropped focusRestorer() entry silently resets the scope to Top Results — cheap to check: Episodes → down into the grid → scroll → back up.
  • Scope-specific empty state: with the Episodes pill selected and a query returning only podcasts, the user sees a bare "no results" while Top Results has content. Correct per scope, but the copy doesn't say which scope is empty.

Verdict: the withFrameNanos {} guard in #1 is the cheapest real fix here, and the episode-actions affordance (#2) is still the one behavioural regression inside the feature this PR adds. Everything else is polish.

Reviewed the diff statically — I did not build the module or run it on a device, so the focus-behaviour notes are reasoning from the modifier chains, not observed.
• branch feat/tv-search-filter

Comment on lines +166 to +169
LaunchedEffect(Unit) {
runCatching { searchFieldFocusRequester.requestFocus() }
}
Column(modifier = modifier.fillMaxSize()) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the one substantive change since the last push, and it's worth calling out because it swaps a declarative focus contract for an imperative one-shot.

Before:

Column(
    modifier = modifier
        .fillMaxSize()
        .focusGroup()
        .focusProperties {
            onEnter = { runCatching { searchFieldFocusRequester.requestFocus() } }
        },
)

I can see why it had to go — onEnter fires every time focus re-enters the screen, so it was fighting restoreFocusTrigger and yanking focus back to the search field the moment the podcast overlay closed. Good change in intent. Two things I'd check before merge, though:

1. LaunchedEffect(Unit) + requestFocus() is the racy variant, and runCatching hides the failure.

FocusRequester.requestFocus() throws if the target node isn't placed yet, and a LaunchedEffect(Unit) body can run before the first layout pass. This repo already knows that — TvEmptyState.kt:41-44 does:

LaunchedEffect(Unit) {
    withFrameNanos {}
    runCatching { focusRequester.requestFocus() }
}

and TvPodcastGridScaffold.kt:55-58 waits on snapshotFlow { …visibleItemsInfo.isNotEmpty() }.first { it } for the same reason. Here there's no guard, so when it loses the race the runCatching swallows it and the Search tab opens with nothing focused — on a TV that means a dead D-pad until the user finds their way back up to the tab bar. It'll be intermittent and device/timing dependent, which is the worst kind. Adding withFrameNanos {} matches the existing precedent and costs one line.

2. tvFocusInactiveWhen now has no focus target of its own to attach to.

tvFocusInactiveWhen is just focusProperties { canFocus = false } (TvDetailOverlay.kt:89-93), and it's applied to the modifier this Column receives (:114). Previously that focusProperties sat directly on the .focusGroup() focus target, so deactivating that one group blocked the entire subtree — which is what the KDoc promises ("Deactivates focus for this subtree"). With focusGroup() gone, this Column has no focus target in its chain, so the deactivation propagates only to whichever focus targets happen to be nearest below it, and stops at each of them.

Note this screen was the only tvFocusInactiveWhen call site with an explicit focusGroup() — the others (TvHomeScreen.kt:81, TvYourPodcastsScreen.kt:69, TvPlaylistsScreen.kt:96) hand it to a root that owns its own grid/list focus target. I'm reasoning from the modifier chain rather than from a device, so treat this as "please verify" rather than a claim: with the podcast detail overlay open over search results, confirm the D-pad genuinely can't move focus among the results underneath in all three scopes (Top Results carousel, Podcasts grid, Episodes grid — the two grids each add their own focusGroup(), so they're the interesting ones). If it can, restoring .focusGroup() without the onEnter gets the subtree contract back while keeping the fix above.

@sztomek

sztomek commented Aug 14, 2026

Copy link
Copy Markdown
Contributor Author

@geekygecko thanks for the feedback, valid points!
updated the pills so they now turn greyish when they are selected but not focused

Screen_recording_20260814_105042.mp4

@sztomek
sztomek force-pushed the feat/tv-search-filter branch from 4cdc38e to eb644a0 Compare August 14, 2026 09:05
@claude

claude Bot commented Aug 14, 2026

Copy link
Copy Markdown

Claude finished @sztomek's task in 4m 6s —— View job


Code review: TV Search Podcasts/Episodes scope filter

  • Gather context (diff vs origin/main, changes in eb644a0)
  • Review TvSearchScreen.kt, TvSearchFilters.kt, TvPodcastGridScaffold.kt, TvSearchEpisodeRow.kt, TvSearchViewModel.kt + test
  • Post inline comment + summary

Re-reviewed after eb644a0. Most of what I raised last pass is now closed:

Previous note Status
LaunchedEffect(Unit) + requestFocus() raced first layout Fixed — withFrameNanos {} guard (:169), matching TvEmptyState
title = "" sentinel Fixed — title: String? = null (TvPodcastGridScaffold.kt:36)
Podcasts grid 16dp out of line with the rest of the screen Fixed — horizontalContentPadding param, search passes 48dp
Modifier.padding(horizontal = 48.dp) re-stating the constant Fixed — ContentHorizontalPadding / ContentPadding (:69-70)
No tests for the new VM state Fixed — both behaviours covered (TvSearchViewModelTest.kt:310-330)
Grey selected-but-unfocused pill (@geekygecko) Fixed in TvTabBar + TvSearchFilters

Also confirmed the TvPodcastGridScaffold signature reorder is safe — both other call sites (TvYourPodcastsScreen.kt:162, TvFolderDetailScreen.kt:98) pass every argument by name.

Retracting one item from my last pass: I flagged that dropping .focusGroup() from TvSearchContent left tvFocusInactiveWhen without a focus target. Having traced the modifier chain properly, that was wrong in direction — focusProperties propagates down to the nearest descendant focus targets, so the two grids' own focusGroup()s now receive canFocus = false exactly as the removed root group did, and the Top Results list gets it on the item targets instead. Same coverage or better. No action needed.


1. Still the one behavioural gap: episode actions are long-press-only in the Episodes scope (thread)

TvSearchEpisodeCard in the grid reaches onOpenEpisodeActions only through onLongClick (:435), with nothing on screen hinting at it. The same episodes one pill over go through TvEpisodeListItemContainer (TvEpisodeListItem.kt:56-84), which animates a visible TvMoreButton in on focus. So narrowing to the scope dedicated to episodes is what removes the discoverable action.

If the grid cell can't spare the horizontal room for the more button, an overflow glyph in the focused card would do. Either way, onLongClickLabel is a one-word addition and gets TalkBack announcing it. Worth confirming on device too that a held centre press now opens the modal instead of playing, since onLongClick suppresses onClick on a long press.

2. Episodes grid padding (inline)

:416 is the last hardcoded 48.dp now that the constant exists two screens away, and unlike the podcast grid it has no top padding — so the first row sits 16dp higher in Episodes than in Podcasts, and the focused card's 1.02f scale gets clipped at the grid's top edge. Suggestion in the thread.

3. TvSearchFilterPills still duplicates TvTabBar (thread)

TvSearchFilters.kt:57-106 vs TvTabBar.kt:79-146: still identical apart from the one .then(…focusRequester…) line, and this push is the second time the same colour change had to be applied to both files. A shared TvPillTabRow in component/ is cheap now and gets less cheap with every tweak. Non-blocking.

4. Smaller, unchanged

  • Filter row renders for Searching / Error / NoResults (:183). I now think this is right — it's what keeps the row from flickering out between keystrokes as Results → Searching → Results — but on Error/NoResults it's a focusable row where every pill shows the same content. A one-line comment would settle the intent.
  • No-results message written three times with identical arguments (:215-218, :296-299, :319-322). A local @Composable fun NoResults() collapses it.
  • Scope-specific empty copy: Episodes selected + a query that returns only podcasts gives a bare "no results" while Top Results has content. Correct per scope, but the copy doesn't say which scope is empty.
  • Preview coverage: TvSearchFilters previews only the middle pill (Podcasts) — a TopResults variant would catch first-pill indicator/rounding — and neither new scope (podcast grid, episode grid) has a preview, so the checklist item isn't quite met.
  • No explicit focus requester on the selected pill, unlike TvTabBar.kt:107. Since selection follows focus, a dropped focusRestorer() entry silently resets the scope to Top Results. Cheap to check on device: Episodes → down into the grid → scroll a few rows → back up to the pills.
  • Global tab-bar colour change (thread) — TvTabBar.kt:94/110 lands on every screen. Requested by @geekygecko and already approved, so just worth a line in the PR description for whoever reads the history.

Verdict: this push cleared everything I'd called blocking or near-blocking except the episode-actions affordance (#1), which is still the one place the feature contradicts itself between scopes. #2 is a two-line fix with a suggestion attached; #3 and #4 are follow-up material.

Reviewed statically — I did not build the module or run it on a device, so the focus-behaviour notes are read off the modifier chains rather than observed.
• branch feat/tv-search-filter

@sztomek
sztomek merged commit 4492b1d into main Aug 14, 2026
21 checks passed
@sztomek
sztomek deleted the feat/tv-search-filter branch August 14, 2026 09:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants